iT邦幫忙

2022 iThome 鐵人賽

DAY 28
0

大綱

  • JavaScript 中函式所扮演的三個角色
  • 術語:「參數」vs.「引數」
  • 定義函示
  • 拉升 (hosting)
  • 函式的名稱
  • 何種較佳:函式宣告或函式運算式 ?
  • 控制函式呼叫 (Function Calls):call()、apply() 與 bind()
  • 處理缺少或額外的參數
  • 具名參數

JavaScript 中函式所扮演的三個角色

Example:

function id(value) {
	return value;
};
id('Hello World'); // Hello World

// ===== 這是分隔線 ===== //

function f() {};
f(); // undefined

非方法的函式,也就是一般的

  • 怎樣呼叫 !? id('Hello World')
  • 依照慣例會以小寫字母開頭,更像是駝峰式命名法 (CamelCase) 來命名。

建構器 (Constructor)

  • 使用 new 運算子來調用一個函式。
  • 依照慣例會以大寫字母開頭。

Example:

new Date()

new String()

new Number()

new Boolean()

new Promise()

new Function()

new Error()

方法

  • 這裡是指將一個函式儲存在一個物件的特性中,當要使用時 obj.method()
  • 依照慣例會以小寫字母開頭。

術語:「參數」vs.「引數」

  • 參數:會被用來定義一個函式

Example:

function add(param1, param2) { // 這裡有兩個參數分別是 param1, param2
	return param1 + param2;
};
  • 引數:會被用來呼叫一個函式。

Example:

add(2, 4); // 這裡有兩個引數分別是 2, 4

定義函式

  • 藉由一個函式運算式 (function expression)。
  • 藉由一個函式宣告 (function declaration)。
  • 藉由建構器函式 Function()。

Example:

function id(x) {
	return x;
};
console.log(id instanceof Function); // true

console.log(typeof id === 'function'); // true

函式運算式

匿名的函式運算式

Example:

var add = function (x, y) {
	return x + y;
};

console.log(add(2, 3));
  • 一個函式運算式最後會估算成一個值,接著賦於給 add 這個變數。
  • 在這個 function 中並沒有名稱,故又可以稱為匿名的函式運算式 (anonymous function expression)

具名的函式運算式

Example:

var fac = function me(n) {
	if (n > 0) {
		return n * me(n-1); // 4 * 3 * 2 * 1
	} else {
		return 1;
	};
};

console.log(fac(3)) // 6
console.log(fac(4)) // 會得到什麼 ?

console.log(me) //注意 ! Reference:me is not defined
  • 賦於函式運算式一個名稱,就可稱為具名的函式運算式。
  • 常用來自我遞迴。

函式宣告

Example:

function add(x, y) {
	return x + y;
};
  • 這是一個函式宣告 (function declaration)。
  • 這是一個陳述式 (statement)。

Function 建構器

Example:

var add = new Function('x', 'y', 'return x + y');

console.log(add(1, 2)); // 3
  • 我的想法是要把程式碼用字串包住,要實作一個 function 時,頗不直覺的。
  • 書中也不建議使用。

拉升 (hosting)

拉升 (hosting) 代表者移動到一個範疇 (scope) 開頭 (頂端) 的地方。

函式宣告會完全被拉升

Example:

console.log(foo()); // 要印出 foo() 可以預期會得到 'Hello'

function foo() { // foo 函式會被拉升
	return 'Hello';
};

// ===== 這是分隔線 ===== //

// 上述的程式,實際執行的流程如下
function foo() {
	return 'Hello';
};

console.log(foo());
  • 因為是函式宣告的用法,JavaScript 引擎會把整個函式拉升到範疇 (scope) 的最頂端,所以,這樣的程式碼是可執行的。

變數宣告只有部分被拉升

Example:

console.log(foo()); // "TypeError: foo is not a function

var foo = function () {
	return 'World';
};

// ===== 這是分隔線 ===== //

// 上述的程式,實際執行的流程如下
var foo; // 1. 變數宣告會被提升到 scope 的最頂端

console.log(foo()); // 2. 執行foo(),會出現錯誤 "TypeError: foo is not a function

console.log(foo); // 會出現什麼 ?

foo = function () {
	return 'World';
};
  • 這是因為拉升只會有變數宣告的部份,後續的指定等運算都不會跟著提升。

變數 vs 函式的拉升

變數與函式的拉升的不同之處在於,變數的拉升只有宣告部份,而函式的拉升是整個函式,因此函式在宣告前是可以執行的。

補充:如果是一般變數宣告呢 ?

Example:

console.log(a); // 會得到 undefined

var a = 2;

// ===== 這是分隔線 ===== //

// 上述的程式,實際執行的流程如下
var a; // 1. 變數宣告會被提升到 scope 的最頂端

console.log(a); // 2. 嘗試印出 a,會得到 undefined

a = 2;
  • 我們可想像成這是因為編譯器會先掃過程式碼中的宣告的變數和函式,而把這些變數和函示「提升」到程式碼的最頂端,因此當印出 a 的值的時候,會是已宣告但還沒賦值的狀態,也就是有這個變數,但其值是 undefined

補充:若同時有多個函式同名,則後面的會覆寫前面的宣告

Example:

foo(); // 會得到什麼 ?

function foo() {
  console.log(1);
};

function foo() {
  console.log(2);
};

function foo() {
  console.log(3);
};

函式的名稱

  • JavaScript 支援 name 特性。

函式宣告

Example:

function add(x, y) {
	return x + y;
};

console.log(add.name) // 'add'

函式運算式

匿名的函式運算式

Example:

var add = function (x, y) {
	return x + y;
};

console.log(add.name) // '書中寫空字串,但是實際上會印出 add'

具名的函式運算式

Example:

var add = function addNumberMethod(x, y) {
	return x + y;
};

console.log(add.name) // 'addNumberMethod'

何種較佳:函式宣告或函式運算式 ?

Example:

// 函式宣告
function add(x ,y) {
	return x + y
}

// 函式運算式
const add = function (x, y) {
	return x + y
}
  • 整個函式都被拉升,因此,可在該函式之前呼叫它。
  • 很明確的具有函式名稱,根據這點而言,就見人見智了。

補充:以我目前公司的專案為例

Example:

export const convertNullToString = (object) => (
  JSON.parse(JSON.stringify(object, (k, v) => (v === null ? '' : v)))
)

export const convertEmptyStringToNull = (object) => (
  JSON.parse(JSON.stringify(object, (k, v) => (v === '' ? null : v)))
)

export const sortStringAsc = (a, b) => { return a >= b ? 1 : -1 }

export const sortStringDesc = (a, b) => { return a <= b ? 1 : -1 }

export const parseDecimal = (value) => {
  if (typeof value === 'number') { return new Decimal(value) }
  if (typeof value !== 'string') { return new Decimal(0) }
  let number = value.replace(/\s/g, '').replace(/,/g, '')
  number = number.replace(/\.+/g, '.').replace(/\.$/, '')
  return new Decimal(number)
}

export const floorToDigits = (amount, digits) => {
  const number = Math.pow(10, digits)
  return Math.floor(amount * number) / number
}

export const stringToArray = (value) => {
  if (Array.isArray(value)) return value
  if (typeof value !== 'string' || value === '') return []
  return value.split(',').map((v) => v.trim())
}

export const arrayToString = (array) => (
  _.isEmpty(array) ? '' : array.filter(element => element).join(', ')
)

export const ModeIcon = ({ cell }) => {
  switch (cell) {
    case 'AIR':
      return <div className='text-info'><i className='fas fa-plane' /></div>
    case 'SEA':
      return <div className='text-info'><i className='fas fa-ship' /></div>
    case 'RAIL':
      return <div className='text-info'><i className='fas fa-train' /></div>
    case 'TRUCK':
      return <div className='text-info'><i className='fas fa-truck' /></div>
    default:
      return { cell }
  }
}

控制函式呼叫 (Function Calls):bind()、apply() 與 call()

  • call()、apply() 與 bind() 是所有函式都會有的方法。
  • 都是用來改變 this 的指向。

func.bind(thisValue, arg1, ..., argN)

  • 使用 bind 會得到一個新的函式。
  • 引數是 arg1..... 一直到 argN。
  • 這裡是非物件導向的情境,我們用不到 thisValue,所以預設會是 null。

Example:

function add1(x, y) {
  return x + y
}

var plus1 = add1.bind(null, 1)

console.log(plus1(5)) // 6

// ========== 這是分隔線 ========== //

function add2(x, y, z) {
  return x + y + z
}

var plus2 = add2.bind(null, 1, 2)

console.log(plus2(5)) // 會得到什麼 ?

補充:關於 bind 用來改變 this 的指向

Example:

var foo = {
  name: 'alan',
  logName: function () {
    console.log(this.name)
  }
}

var bar = {
  name: 'mike'
}

console.log(foo.logName()) // 會得到什麼 ?

console.log(foo.logName.bind(bar)()) // 會得到什麼 ?

func.apply(thisValue, argArray)

  • 這裡是非物件導向的情境,我們用不到 thisValue,所以預設會是 null。
  • 引數是一個陣列 [arg1..... 一直到 argN]。

Example:

function add1(x, y) {
  return x + y
}

var plus1 = add1.apply(null, [1, 5])

console.log(plus1) // 會得到什麼 ?

補充:關於 apply 用來改變 this 的指向

Example:

const foo = {
  name: 'alan',
  logName: function () {
    console.log(this.name)
  }
}

const bar = {
  name: 'mike'
}

console.log(foo.logName()) // 會得到什麼 ?

console.log(foo.logName.apply(bar)) // 會得到什麼 ?

補充:func.call(thisValue, arg1, ..., argN)

  • 這裡是非物件導向的情境,我們用不到 thisValue,所以預設會是 null。
  • 引數是 arg1..... 一直到 argN。

Example:

function add1(x, y) {
  return x + y
}

var plus1 = add1.call(null, 1, 5)

console.log(plus1) // 6

Example:

const foo = {
  name: 'soto',
  logName: function () {
    console.log(this.name)
  }
}

const bar = {
  name: 'mike'
}

console.log(foo.logName()) // 'soto'

console.log(foo.logName.call(bar)) // 'mike'

補充:結論-關於 bind、call 與 apply 改變 this 指向

  • 面試很常會考到這三的差別。
  • 這三者的共同點,都是用來改變相關函式 this 的指向。
  • call 與 apply 是直接進行相關函式呼叫的,bind 不會執行相關函式,而是會回傳一個新的函式。
  • call 與 apply 這兩者的差別主要是關於參數設定上。
// 1
var sampleObj = {}

fn.call(sampleObj, 'arg1', 'arg2')

// 2
var sampleObj = {}

fn.apply(sampleObj, ['arg1', 'arg2'])

// 3
var sampleObj = {}

fn.bind(sampleObj, 'arg1', 'arg2')()
var fn = function (patam1, param2) {
  console.log(this.key1) // 會得到 1
  console.log(patam1)    // 'arg1'
  console.log(param2)    // 'arg2' 
}

// 1
var sampleObj = {key1: 1}

fn.call(sampleObj, 'arg1', 'arg2')

// 2
var sampleObj = {key1: 2}

fn.apply(sampleObj, ['arg1', 'arg2'])

// // 3
var sampleObj = {key1: 3}

fn.bind(sampleObj, 'arg1', 'arg2')()

處理缺少或額外的參數

實際參數比形式參數還要多

  • 多餘的參數會被忽略掉。

Example:

function logArgs(x, y, z) {
  console.log(x)
  console.log(y)
  console.log(z)
}

logArgs('Hello', 'World', 'React', 'JavaScript') // 'JavaScript' 被忽略掉

實際參數比形式參數還要少

  • 缺少的形式參數會是 undefined。

Example:

function logArgs(x, y, z) {
  console.log(x)
  console.log(y)
  console.log(z) // undefined
}

logArgs('Hello', 'World')

藉由索引存取所有的參數:特殊變數 arguments

  • 類陣列 (array-like),具有特性 length,並且可透過索引來讀寫。
  • 又不具有陣列的任何方法。書中後面章節可以把 arguments 轉為真正的陣列。

Example:

function logArgs(x, y, z) {
  console.log(arguments) // *['Hello', 'World', 'React'] <== 是斜的*
	console.log(arguments.length) // 3
  console.log(arguments[0]) // 'Hello'
  console.log(arguments[1]) // 'World'
}

logArgs('Hello', 'World', 'React')

arguments 被棄用的功能

  • arguments.callee 指向目前的函式,常用在匿名函式中做自我遞迴。
  • 在嚴格模式 (strict) 被禁用,解決方法是使用具名函式運算式。
  • 在非嚴格模式 (strict) 中,變更了參數的值,arguments 中的值也會跟著改變。

Example:非 strict

function funa(param) {
  param = 'Hello'
  
  return arguments[0]
}

console.log(funa('value')) // 'Hello'

Example:strict

function funb(param) {
  'use strict'
  param = 'Hello'
  
  return arguments[0]
}

console.log(funb('value')) // 會得到什麼 ?
  • 在嚴格模式 (strict) 中,禁止重新賦值給變數 arguments,但是,做一些動作是可行的。

Example:

function funb(param) {
  'use strict'
  param = 'Hello' // 無用

	return [
		arguments[0].toUpperCase(), // 'VALUE'
		arguments[0].split(''), // ["v", "a", "l", "u", "e"]
		arguments[0].length // 5
	]
}

console.log(funb('value'))

選擇性參數

這裡是指針對一個函式的參數是有選擇性 (有給或不給) 的話,我們通常會給一個預設值,以下有四種 + 一 種 (ES6) 檢查的方式。

一、檢查 undefined

Example:

function message(option1, option2, option3) {
	if (option3 === undefined) option3 = 'default value'
	
	return option1 + option2 + option3
}

console.log(message('Hello', 'World'))

二、將 optional 解讀為一個 boolean 值

Example:

function message(option1, option2, option3) {
	if (!option3) option3 = 'default value'
	
	return option1 + option2 + option3
}

console.log(message('Hello', 'World'))

三、使用 Or 運算子 ||

Example:

function message(option1, option2, option3) {
	option3 = option3 || 'default value'
	
	return option1 + option2 + option3
}

console.log(message('Hello', 'World'))

四、藉由 arguments.length 來檢查

Example:

function message(option1, option2, option3) {
	if (arguments.length < 3) option3 = 'default value'
	
	return option1 + option2 + option3
}

console.log(message('Hello', 'World'))

補充:五、ES6 傳入參數的預設值指定語法

Example:

function message(option1, option2, option3 = 'default value') {
	return option1 + option2 + option3
}

console.log(message('Hello', 'World'))
  • 第一跟第三種判斷的方法,無法分辨是 message('Hello', 'World') 還是 message('Hello', 'World', undefined) ,在這兩個情況裡,都會得到 option3 = 'default value'
  • 第四種方法,使用 message('Hello', 'World') 會得到 option3 = 'default value' ,但是,如果是 message('Hello', 'World', undefined) 的話,反而會將 option3 的值視為 undefined ,進而得到 'HelloWorldundefined' 這種奇怪的值。

具名參數

  • 位置型參數:顧名思義就是以位置來對應。第一個實際參數會對應到形式參數,依此類推。
  • 具名參數:使用名稱 (標籤) 來對應。因為是透過名稱來對應,所以,順序就不重要,只要名稱有對應到就行。

具名參數做為說明

情境:

如果有一個函式是 selectOptions(10, 20, 5),在呼叫的時候帶了 3 個引數,可是會看不出來這是什麼意思。

以 Pyton 為例:

Pyton 支援具名參數用法,因此在 Pyton 的世界裡,此函式會是。

selectOptions(start = 10, end = 20, step = 5)

選擇性的具名參數

選擇性位置參數:

如果有一個函式是selectOptions(10, null, 5),今天第二個參數不帶值,第三個參數要帶值,哪就必須使用 null 來佔位,卡住第二個參數的位置。

以 Pyton 為例:

如果是 Pyton 的話,可以這樣寫。

Example:

selectOptions(step = 5) // 不需要使用 null 來佔位

selectOptions(end = 20, start = 10) // 還可以互換順序

在 JavaScript 中模擬具名參數

  • JavaScript 不支援具名參數。
  • 可以使用物件的方式,當作參數傳入。

以 JavaScript 為例:

Example:

function selectOptions(options = {}) {
  const { start } = options || 0
  const { end } = options || 0
  const { step } = options || 0
    
  console.log(start) // 10
  console.log(end) // 20
  console.log(step) // 5
}

selectOptions({start: 10, end: 20, step: 5})

位置參數與具名參數的結合一起使用,以 JavaScript 為例:

  • 慣例是具名參數放最後。

Example:

selectOptions(option1, option2, {start: 10, end: 20, step: 5})

上一篇
Day 27 | Variables: Scopes, Environments, and Closures
下一篇
Day 29 | Booleans
系列文
一步一腳印,我的前端工程師修煉30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言